package com.gitee.wsl.net.ssdp.description

import com.gitee.wsl.logger.Logger
import com.gitee.wsl.logger.debug
import com.gitee.wsl.logger.error
import com.gitee.wsl.net.ssdp.AddNotification
import io.ktor.client.HttpClient
import io.ktor.client.engine.HttpClientEngine
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.HttpRequestRetry
import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logging
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText
import io.ktor.http.ContentType
import io.ktor.http.HttpHeaders
import io.ktor.http.URLBuilder
import io.ktor.http.Url
import io.ktor.http.charset
import io.ktor.utils.io.charsets.Charset
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.mapNotNull
import com.gitee.wsl.net.ssdp.description.model.DeviceManifest
import com.gitee.wsl.net.ssdp.description.model.xml.XmlDeviceDescription
import com.gitee.wsl.net.ssdp.description.model.xml.XmlDeviceDescription.Companion.toXmlDeviceDescription
import com.gitee.wsl.net.ssdp.description.model.xml.XmlServiceDescription
import com.gitee.wsl.net.ssdp.description.model.xml.XmlServiceDescription.Companion.toXmlServiceDescription
import com.gitee.wsl.net.ssdp.description.xml.XmlElement
import com.gitee.wsl.net.ssdp.description.xml.XmlParser.parseAsXml
import java.util.Locale

object DescriptionService: IDescriptionService{
    private var client = HttpClient(CIO) {
        install(HttpTimeout) {
            requestTimeoutMillis = 35000
        }
        install(HttpRequestRetry) {
            retryOnExceptionOrServerErrors(maxRetries = 3)
            exponentialDelay()
        }
        /*     install(ContentNegotiation) {
                 xml(format = XML {
                     xmlDeclMode = XmlDeclMode.Charset
                     repairNamespaces = true
                 }, ContentType.Any)
             }

         */
        install(Logging) {
            level = LogLevel.ALL
        }
        /*  real-life requests fail with this configuration (described in the UPnP standard)
        defaultRequest {
            header(
                HttpHeaders.UserAgent,
                "${getOS()} UPnP/2.0 ${getControlPointFriendlyName()}/${getControlPointVersion()}"
            )
            header(HttpHeaders.AcceptEncoding, "compress, gzip")
            header("CPFN.UPNP.ORG", getControlPointFriendlyName())
            header(HttpHeaders.Host, getDeviceHostname())
            header(HttpHeaders.AcceptLanguage, getControlPointPreferredLanguage())
            //header(HttpHeaders.Accept, "application/xml; text/xml")
        }
         */
    }
    // required for tests
    fun setClientEngine(engine: HttpClientEngine) {
        client = HttpClient(engine)
    }

    suspend fun getDeviceDescription(location: Url): XmlDeviceDescription = getDescription(location)
        .toXmlDeviceDescription()

    suspend fun getServiceDescription(location: Url): XmlServiceDescription = getDescription(location)
        .toXmlServiceDescription()


    override suspend fun createCompleteDeviceManifest(usn: String, location: String): DeviceManifest {
        val locationBuilder = URLBuilder(location)
        val locationUrl = locationBuilder.build()
        val descr = getDeviceDescription(locationUrl)

        val metadata = DeviceManifest.Metadata(
            language = Locale.of(descr.language),
            location = locationUrl,
            urns = descr.urn?.let { mapOf("" to it) } ?: emptyMap(),
            specVersion = descr.specVersion,
            systemVersion = descr.systemVersion,
            configId = descr.configId
        )

        val rootDevice = DeviceManifest.Device.fromDescription(descr.device, locationBuilder).apply {
            services.forEach { service ->
                try {
                    service.extendWith(getServiceDescription(service.scpdUrl), service.scpdUrl)
                } catch (ex: Exception) {
                    Logger.error("Failed to extend service with description: ${ex.message}" )
                    service.recordError(ex.message ?: "Unknown error", service.scpdUrl)
                }
            }
        }

        return DeviceManifest(usn, metadata, rootDevice)
    }

    private suspend fun getDescription(location: Url): XmlElement {
        val resp = client.get(location)
        if (resp.status.value > 400) {
            throw Exception("Failed to retrieve description from $location. Device responded with ${resp.status}")
        }
        val charset = resp.headers[HttpHeaders.ContentType]?.let { ContentType.parse(it).charset() }
            ?: Charset.defaultCharset()
        return resp.bodyAsText(charset).parseAsXml()
    }

    fun Flow<AddNotification>.createDeviceManifest() = this.mapNotNull {
        try {
            Logger.debug("New ADD device notification. USN: ${it.usn}, location: ${it.location}")
            createCompleteDeviceManifest(it.usn, it.location)
        } catch (ex: Exception) {
            Logger.error("Failed to create device manifest", ex)
            null
        }
    }
}